using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Meebey.SmartIrc4net;

namespace Pricom
{
  namespace ConnectionProviders
  {
    class SmartIrc : IConnectionProvider
    {
      public SmartIrc()
      {
        irc.SendDelay = 200;
        irc.AutoRetry = true;
        irc.ChannelSyncing = true;
        irc.CtcpVersion = "PriCom";

        mainLoopThread = new Thread(new ThreadStart(MainLoop));
        mainLoopThread.Start();

        irc.OnQueryMessage += new Meebey.SmartIrc4net.Delegates.MessageEventHandler(OnMessage);
        irc.OnChannelMessage += new Meebey.SmartIrc4net.Delegates.MessageEventHandler(OnMessage);
        irc.OnJoin += new Meebey.SmartIrc4net.Delegates.JoinEventHandler(OnJoined);
        irc.OnNameReply += new Meebey.SmartIrc4net.Delegates.NameReplyEventHandler(OnNames);
        irc.OnTopicChange += new Meebey.SmartIrc4net.Delegates.TopicChangeEventHandler(OnTopicChange);
        irc.OnPart += new Meebey.SmartIrc4net.Delegates.PartEventHandler(OnPart);
        irc.OnQuit += new Meebey.SmartIrc4net.Delegates.QuitEventHandler(OnQuit);
        irc.OnNickChange += new Meebey.SmartIrc4net.Delegates.NickChangeEventHandler(OnNickChange);
        irc.OnOp += new Meebey.SmartIrc4net.Delegates.OpEventHandler(OnOpChanged);
        irc.OnDeop += new Meebey.SmartIrc4net.Delegates.DeopEventHandler(OnOpChanged);
      }

      ~SmartIrc()
      {
        irc.Disconnect();
      }

      public void SetProxy(string proxyserver)
      {
        irc.SetProxy(proxyserver);
      }

      public void Connect(string server, int port, string nick)
      {
        if (IsConnected())
          return;

        connectTime = DateTime.Now;
        SetState(ConnectionState.Connecting);
        if (irc.Connect(new string[] { server }, port))
          irc.Login(nick, nick);
      }

      void OnOpChanged(string channel, string who, string whom, Data ircdata)
      {
        string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + channel;
        if (ActionMessageReceived != null)
          ActionMessageReceived.Invoke(this, channel, url, who + " sets mode: +o " + whom);
        if (UserlistChanged != null)
          UserlistChanged.Invoke(this, channel, url, GetUserList(channel));
      }

      void OnNickChange(string oldnickname, string newnickname, Data ircdata)
      {
        foreach (string channel in irc.GetChannels())
        {
          if (channelUsers.ContainsKey(channel))
          {
            channelUsers[channel].Remove(oldnickname);
            channelUsers[channel].Add(newnickname);
          }
          string[] userlist = GetUserList(channel);
          for (int i = 0; i < userlist.Length; i++)
            if (userlist[i] == oldnickname)
            {
              userlist[i] = newnickname;
              string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + channel;
              if (UserlistChanged != null)
                UserlistChanged.Invoke(this, channel, url, userlist);
              if (ActionMessageReceived != null)
                ActionMessageReceived.Invoke(this, channel, url, oldnickname + " is now known as " + newnickname);
              if (oldnickname == nickname) // we changed our
              {
                string newUrl = newnickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + channel;
                if (ConnectionChanged != null)
                  ConnectionChanged.Invoke(this, url, newUrl);
              }
            }
        }
        if (oldnickname == nickname) // we changed our
          nickname = newnickname;
        else
        {
          string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + oldnickname;
          string newUrl = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + newnickname;
          if (ConnectionChanged != null)
            ConnectionChanged.Invoke(this, url, newUrl);
        }
      }

      string[] GetUserList(string channel)
      {
        if (channel == null || irc.GetChannel(channel) == null)
          return new string[0];
        Hashtable users = irc.GetChannel(channel).Users;
        string[] userlist = new string[users.Count];
        int i = 0;
        foreach (ChannelUser user in users.Values)
          userlist[i++] = String.Concat(user.IsOp ? "@" : String.Empty, user.Nick);
        return userlist;
      }

      void OnQuit(string who, string quitmessage, Data ircdata)
      {
        string quitmsg = "";
        if (quitmessage != null && quitmessage.Length != 0)
          quitmsg = " (" + quitmessage + ")";
        if (ircdata.Channel == null)
          foreach (string channel in irc.GetChannels())
          {
            if (channelUsers.ContainsKey(channel) && channelUsers[channel].Contains(who))
            {
              channelUsers[channel].Remove(who);
              string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + channel;
              if (ActionMessageReceived != null)
                ActionMessageReceived.Invoke(this, channel, url, who + " has left " + ircdata.Channel + quitmsg);
              if (UserlistChanged != null)
                UserlistChanged.Invoke(this, channel, url, GetUserList(channel));
            }
          }
        else
        {
          foreach (KeyValuePair<string, List<string>> iterator in channelUsers)
            channelUsers[iterator.Key].Remove(who);
          string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + ircdata.Channel;
          if (ActionMessageReceived != null)
            ActionMessageReceived.Invoke(this, ircdata.Channel, url, who + " has left " + ircdata.Channel + quitmsg);
          if (UserlistChanged != null)
            UserlistChanged.Invoke(this, ircdata.Channel, url, GetUserList(ircdata.Channel));
        }
      }

      void OnPart(string channel, string who, string partmessage, Data ircdata)
      {
        string partmsg = "";
        if (partmessage != null && partmessage.Length != 0)
          partmsg = " (" + partmessage + ")";

        if (channelUsers.ContainsKey(channel))
          channelUsers[channel].Remove(who);

        string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + channel;

        if (who != nickname && ActionMessageReceived != null)
        {
          ActionMessageReceived.Invoke(this, channel, url, who + " has left " + channel + partmsg);
          // csak azokra a channelekre amelyeken fent volt...
        }
        //tabwindow is closed, before this ActionMessageReceived invoke...
        //else if (ActionMessageReceived != null)
        //  ActionMessageReceived.Invoke(this, channel, url, "You have left " + channel + partmsg);
        if (UserlistChanged != null)
          UserlistChanged.Invoke(this, channel, url, GetUserList(channel));
      }

      void OnTopicChange(string channel, string who, string newtopic, Data ircdata)
      {
        string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + channel;
        if (TopicChanged != null)
          TopicChanged.Invoke(this, channel, url, newtopic, who);
      }

      void OnNames(string channel, string[] userlist, Meebey.SmartIrc4net.Data ircdata)
      {
        SetState(ConnectionState.Connected);
        string url = ToString() + "/" + channel; 
        if (UserlistChanged != null)
          UserlistChanged.Invoke(this, channel, url, userlist);
        if (!channelUsers.ContainsKey(channel))
          channelUsers.Add(channel, new List<string>());
        foreach (string user in userlist)
          channelUsers[channel].Add(user);
      }

      void OnJoined(string channel, string who, Meebey.SmartIrc4net.Data ircdata)
      {
        SetState(ConnectionState.Connected);
        string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + channel;
        if (nickname.Length == 0 || who == nickname)
        {
          // we are joined
          nickname = who;
          url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + channel;
          if (UserlistChanged != null)
            UserlistChanged.Invoke(this, channel, url, new string[] { nickname });
          if (ConnectionChanged != null)
            ConnectionChanged.Invoke(this, url, url);
          if (ActionMessageReceived != null)
            ActionMessageReceived.Invoke(this, channel, url, "Now talking in " + channel);
          if (TopicChanged != null)
          {
            string topic = GetTopic(channel);
            TopicChanged.Invoke(this, channel, url, topic, "");
          }
        }
        else
        {
          // someone joined to a channel that we listen
          if (channelUsers.ContainsKey(channel) && !channelUsers[channel].Contains(who))
            channelUsers[channel].Add(who);
          if (UserlistChanged != null)
            UserlistChanged.Invoke(this, channel, url, GetUserList(channel));
          if (ActionMessageReceived != null)
            ActionMessageReceived.Invoke(this, channel, url, who + " has joined " + channel);
        }
      }

      void OnMessage(Meebey.SmartIrc4net.Data ircdata)
      {
        string s = " <" + ircdata.Nick + "> ";
        s += ircdata.Message;
        string from = ircdata.Channel;
        if (ircdata.Type == Meebey.SmartIrc4net.ReceiveType.QueryMessage)
          from = ircdata.Nick;
        string url = nickname + "@" + ircdata.Irc.Address + ":" + ircdata.Irc.Port.ToString() + "/" + from;
        testConnectionStringReceived = (ircdata.Message == testConnectionString);
        if (state == ConnectionState.Reconnecting)
        {
          SetState(ConnectionState.Connected);
          testConnectionStringReceived = testConnectionStringReceived || (ircdata.Message.StartsWith(testConnectionStringPrefix));
        } 
        if (MessageReceived != null && !ircdata.Message.StartsWith(testConnectionStringPrefix))
          MessageReceived.Invoke(this, from, url, s);
      }

      public void Command(string msg)
      {
        irc.WriteLine(msg);
      }
      public void Send(string to, string msg)
      {
        if (Config.main.messages.sendUTF8)
        {
          byte[] bytesUni = Encoding.Unicode.GetBytes(msg);
          byte[] bytes8 = Encoding.Convert(Encoding.Unicode, Encoding.UTF8, bytesUni);
          string message8 = string.Empty;
          for (int i = 0; i < bytes8.Length; ++i)
            if (Config.main.messages.fixUTF8bug152)
              message8 += i > 0 && bytes8[i - 1] == (byte)0xc3 && (byte)bytes8[i] == (byte)0xbc ? (char)0x152 : (char)bytes8[i];
            else
              message8 += (char)bytes8[i];
          irc.Message(SendType.Message, to, message8);
        }
        else
          irc.Message(SendType.Message, to, msg);
      }
      public void Join(string channel)
      {
        irc.Join(channel);
      }
      public void Leave(string channel)
      {
        irc.Part(channel);
      }
      public void Disconnect()
      {
        if (mainLoopThread != null)
          mainLoopThread.Abort();
        irc.Disconnect();
      }
      public bool IsConnected()
      {
        return irc.Connected;
      }
      public string GetNickname()
      {
        return nickname;
      }

      public string GetTopic(string channel)
      {
        if (irc == null || irc.GetChannel(channel) == null)
          return string.Empty;
        return irc.GetChannel(channel).Topic;
      }

      public override string ToString()
      {
        string host = irc.Address;
        string port = irc.Port.ToString();
        return nickname + "@" + host + ":" + port;
      }

      enum ConnectionState
      {
        Reset,
        Connecting,
        ConnectionLost,
        Reconnecting,
        Connected,
      };

      // todo: connecting timeout
      private void MainLoop()
      {
        DateTime time = DateTime.Now;
        while (true)
        {
          irc.Listen(false);
          Thread.Sleep(1);

          if ((DateTime.Now - time).TotalMilliseconds < 2000)
            continue;
          time = DateTime.Now;

          if (state == ConnectionState.Reconnecting)
          {
            if (TestConnection())
            {
              string[] channels = irc.GetChannels();
              foreach (string channel in channels)
              {
                string url = ToString() + "/" + channel;
                ActionMessageReceived.Invoke(this, channel, url, "Reconnected...");
              }
              SetState(ConnectionState.Connected);

            } 
            else if ((DateTime.Now - connectTime).TotalMilliseconds > 4000)
            {
              SetState(ConnectionState.ConnectionLost);
              System.Diagnostics.Debug.WriteLine("reconnect");
            }
          }
          else if (state == ConnectionState.ConnectionLost)
          {
            string server = irc.Address;
            int port = irc.Port;
            string[] channels = irc.GetChannels();
            ++reconnectCount;
            foreach (string channel in channels)
            {
              string url = ToString() + "/" + channel;
              string counter = (reconnectCount < 2 ? "" : " (" + reconnectCount.ToString() + ") ");
              ActionMessageReceived.Invoke(this, channel, url, "Reconnecting" + counter + "...");
              channelUsers.Clear();
            }
            irc.Disconnect();
            Connect(server, port, nickname);
            testConnectionString = string.Empty;
            TestConnection();
            SetState(ConnectionState.Reconnecting);
          }
          else if (state != ConnectionState.Connecting && !TestConnection())
          {
            string[] channels = irc.GetChannels();
            foreach (string channel in channels)
            {
              string url = ToString() + "/" + channel;
              ActionMessageReceived.Invoke(this, channel, url, "Connection lost...");
              SetState(ConnectionState.ConnectionLost);
              reconnectCount = 0;
            }
          }
        }
      }

      public bool TestConnection()
      {
        if (testConnectionStringReceived)
          testConnectionString = string.Empty;
        if (testConnectionString.Length == 0)
        {
          testConnectionString = testConnectionStringPrefix + System.DateTime.Now.ToString();
          testConnectionStringReceived = false;
          Send(nickname, testConnectionString);
          return true;// !reconnecting;
        }
        return testConnectionStringReceived;
      }

      private void SetState(ConnectionState newState)
      {
        state = newState;
      }

      // main
      private Thread mainLoopThread;
      private IrcClient irc = new IrcClient();

      // session
      private string nickname = "";
      private Dictionary<string, List<string>> channelUsers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
      private DateTime connectTime;

      // connection 
      private string testConnectionString = string.Empty;
      private string testConnectionStringPrefix = "pricom connection test at ";
      private bool testConnectionStringReceived = false;
      private int reconnectCount = 0;
      private ConnectionState state = ConnectionState.Reset;

      // events
      public event UserlistChangedEventHandler UserlistChanged;
      public event TopicChangedEventHandler TopicChanged;
      public event MessageReceivedEventHandler MessageReceived;
      public event MessageReceivedEventHandler ActionMessageReceived;
      public event ConnectionChangedEventHandler ConnectionChanged;
    }
  }
}